// Written in the D Programming Language
/**
 * The 21st lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=21">NeHe tutorial series</a>.
 * Originally written by Jeff Molofee.
 *
 * Authors: Jeff Molofee
 *          Olli Aalto
 */
module lesson21;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;
import derelict.sdl.mixer;

import tango.stdc.stringz;
import tango.math.random.Kiss;
import tango.io.Stdout;
import Int = tango.text.convert.Integer;

/// The window title
const char[] WINDOW_TITLE = "NeHe's Line Tutorial (D version)";

/// The main loop flag
bool running;

/// Our audio chunk
Mix_Chunk* chunk;
Mix_Music* music;

/// Keeps Track Of Verticle Lines
bool[11][11] vline;
/// Keeps Track Of Horizontal Lines
bool[11][11] hline;
/// Done Filling In The Grid?
bool filled;
/// Is The Game Over?
bool gameover;
/// Antialiasing?
bool anti = true;

/// Enemy Delay
int delay;
/// Speed Adjustment For Really Slow Video Cards
int adjust = 3;
/// Player Lives
int lives = 5;
/// Internal Game Level
int level = 1;
/// Displayed Game Level
int displayedLevel = 1;
/// Game Stage
int stage = 1;
/// The width of the window
int windowWidth = 640;
/// The heigth of the window
int windowHeigth = 480;

/// The random number generator
Kiss kiss;

/// Create a structure for our game objects
struct GameObject
{
	int fx, fy; // Fine Movement Position
	int x, y; // Current Player Position
	float spin = 0.0f; // Spin Direction
}

/// Player Information
GameObject player;
/// Enemy Information
GameObject[] enemies;
/// Hourglass Information
GameObject hourGlass;

/// Stepping Values For Slow Video Adjustment
const steps = [1, 2, 4, 5, 10, 20];

/// Number of textures
const NUM_TEXTURES = 2;

/// Storage for textures
GLuint[NUM_TEXTURES] textures;
/// Base Display List For The Font
GLuint base;

/**
 * Module constructor. Here we load the GL, GLU, SDL and SDL Mixer shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();
	DerelictSDLMixer.load();

	if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
	// Enable key repeating
	if ((SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY,
		SDL_DEFAULT_REPEAT_INTERVAL)))
	{
		throw new Exception("Failed to set key repeat: " ~ getSDLError());
	}

	kiss = Kiss();
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	// Clean up our font list
	glDeleteLists(base, 256);

	// Clean up our textures
	glDeleteTextures(NUM_TEXTURES, &textures[0]);

	// Stop playing the music
	Mix_HaltMusic();

	// Free up the memory for the music
	Mix_FreeMusic(music);

	// Free up any memory for the sfx
	Mix_FreeChunk(chunk);

	// Close our audio device
	Mix_CloseAudio();

	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	// Open the sound device
	if (Mix_OpenAudio(22060, AUDIO_S16SYS, 2, 512) < 0)
	{
		throw new Exception("Unable to open audio: " ~ getMixError());
	}

	createGLWindow(WINDOW_TITLE, windowWidth, windowHeigth, 16, fullScreen);
	initGL();

	// reset our objects
	resetObjects();

	// Load in the music
	music = Mix_LoadMUS("data/lktheme.mod");
	// Start playing the music
	Mix_PlayMusic(music, -1);

	running = true;
	while (running)
	{
		processEvents();
		logic();
		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_VIDEORESIZE:
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process a key released event.
 * 
 * Params:
 *     key = The released key
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		case SDLK_SPACE:
			if (gameover)
			{
				gameover = false; // gameover becomes false
				filled = true; // filled becomes true
				level = 1; // Starting Level Is Set Back To One
				displayedLevel = 1; // Displayed Level Is Also Set To One
				stage = 1; // Game Stage Is Set To Zero
				lives = 5; // Lives Is Set To Five
			}
			break;
		case SDLK_a:
			anti = !anti;
			break;
		default:
			break;
	}
}

/**
 * Process a key pressed event.
 * 
 * Params:
 *     key = The pressed key
 */
void keyPressed(int key)
{
	switch (key)
	{
		case SDLK_RIGHT:
			if ((player.x < 10) && (player.fx == player.x * 60) && (player.fy == player.y * 40))
			{
				// Mark The Current Horizontal Border As Filled
				hline[player.x][player.y] = true;
				// Move The Player Right
				player.x++;
			}
			break;
		case SDLK_LEFT:
			if ((player.x > 0) && (player.fx == player.x * 60) && (player.fy == player.y * 40))
			{
				// Move The Player Left
				player.x--;
				// Mark The Current Horizontal Border As Filled
				hline[player.x][player.y] = true;
			}
			break;
		case SDLK_UP:
			if ((player.y > 0) && (player.fx == player.x * 60) && (player.fy == player.y * 40))
			{
				// Move The Player Up
				player.y--;
				// Mark The Current Verticle Border As Filled
				vline[player.x][player.y] = true;
			}
			break;
		case SDLK_DOWN:
			if ((player.y < 10) && (player.fx == player.x * 60) && (player.fy == player.y * 40))
			{
				// Mark The Current Verticle Border As Filled
				vline[player.x][player.y] = true;
				// Move The Player Down
				player.y++;
			}
			break;
		default:
			break;
	}
}

void logic()
{
	if (!gameover)
	{
		// Move the enemies
		foreach (inout enemy; enemies)
		{
			// Move The Enemy Right
			if ((enemy.x < player.x) && (enemy.fy == enemy.y * 40))
			{
				enemy.x++;
			}

			// Move The Enemy Left
			if ((enemy.x > player.x) && (enemy.fy == enemy.y * 40))
			{
				enemy.x--;
			}

			// Move The Enemy Down
			if ((enemy.y < player.y) && (enemy.fx == enemy.x * 60))
			{
				enemy.y++;
			}

			// Move The Enemy Up
			if ((enemy.y > player.y) && (enemy.fx == enemy.x * 60))
			{
				enemy.y--;
			}

			// Are Any Of The Enemies On Top Of The Player?
			if ((enemy.fx == player.fx) && (enemy.fy == player.fy))
			{
				// Player Loses A Life
				lives--;

				// Are We Out Of Lives?
				if (lives == 0)
				{
					gameover = true;
				}

				// Play The Death Sound
				playSound("data/die.wav", 0);

				resetObjects();
			}
		}

		// Should the enemies move?
		if (delay > (3 - level) && (hourGlass.fx != 2))
		{
			// Reset The Delay Counter Back To Zero
			delay = 0;
			foreach (inout enemy; enemies)
			{
				// Is Fine Position On X Axis Lower Than Intended Position?
				if (enemy.fx < enemy.x * 60)
				{
					// Increase Fine Position On X Axis
					enemy.fx += steps[adjust];
					// Spin Enemy Clockwise
					enemy.spin += steps[adjust];
				}

				// Is Fine Position On X Axis Higher Than Intended Position?
				if (enemy.fx > enemy.x * 60)
				{
					// Decrease Fine Position On X Axis
					enemy.fx -= steps[adjust];
					// Spin Enemy Counter Clockwise 
					enemy.spin -= steps[adjust];
				}

				// Is Fine Position On Y Axis Lower Than Intended Position?
				if (enemy.fy < enemy.y * 40)
				{
					// Increase Fine Position On Y Axis
					enemy.fy += steps[adjust];
					// Spin Enemy Clockwise
					enemy.spin += steps[adjust];
				}

				// Is Fine Position On Y Axis Higher Than Intended Position?
				if (enemy.fy > enemy.y * 40)
				{
					// Decrease Fine Position On Y Axis
					enemy.fy -= steps[adjust];
					// Spin Enemy Counter Clockwise
					enemy.spin -= steps[adjust];
				}
			}
		}

		// Move the player
		// Is Fine Position On X Axis Lower Than Intended Position?
		if (player.fx < player.x * 60)
		{
			// Increase The Fine X Position
			player.fx += steps[adjust];
		}

		// Is Fine Position On X Axis Greater Than Intended Position?
		if (player.fx > player.x * 60)
		{
			// Decrease The Fine X Position
			player.fx -= steps[adjust];
		}

		// Is Fine Position On Y Axis Lower Than Intended Position?
		if (player.fy < player.y * 40)
		{
			/* Increase The Fine Y Position */
			player.fy += steps[adjust];
		}

		// Is Fine Position On Y Axis Lower Than Intended Position?
		if (player.fy > player.y * 40)
		{
			// Decrease The Fine Y Position
			player.fy -= steps[adjust];
		}
	}

	// Is The Grid Filled In?
	if (filled)
	{
		// Play The Level Complete Sound
		playSound("data/complete.wav", 0);

		// Increase The Stage
		stage++;

		// Is The Stage Higher Than 3?
		if (stage > 3)
		{
			stage = 1; // If So, Set The Stage To One 
			level++; // Increase The Level
			displayedLevel++; // Increase The Displayed Level

			// Is The Level Greater Than 3?
			if (level > 3)
			{
				// Set The Level To 3
				level = 3;
				// Give The Player A Free Life
				lives++;

				// Player Have More Than 5 Lives?
				if (lives > 5)
				{
					lives = 5; // Set Lives To Five
				}
			}
		}

		// Reset Player / Enemy Positions
		resetObjects();

		// Loop Through The Grid X Coordinates
		for (int loop1 = 0; loop1 < 11; loop1++)
		{
			// Loop Through The Grid Y Coordinates
			for (int loop2 = 0; loop2 < 11; loop2++)
			{
				// If X Coordinate Is Less Than 10 
				if (loop1 < 10)
				{
					// Set Horizontal Value To false
					hline[loop1][loop2] = false;
				}

				// If Y Coordinate Is Less Than 10
				if (loop2 < 10)
				{
					// Set Vertical Value To false
					vline[loop1][loop2] = false;
				}
			}
		}
	}

	// If The Player Hits The Hourglass While It's Being Displayed On The Screen
	if ((player.fx == hourGlass.x * 60) && (player.fy == hourGlass.y * 40) && (hourGlass.fx == 1))
	{
		// Play Freeze Enemy Sound
		playSound("data/freeze.wav", -1);

		// Set The hourglass fx Variable To Two
		hourGlass.fx = 2;
		// Set The hourglass fy Variable To Zero
		hourGlass.fy = 0;
	}

	// Spin The Player Clockwise
	player.spin += 0.5f * steps[adjust];

	// Is The spin Value Greater Than 360?
	if (player.spin > 360.0f)
	{
		player.spin -= 360;
	}

	// Spin The Hourglass Counter Clockwise
	hourGlass.spin -= 0.25f * steps[adjust];

	// Is The spin Value Less Than 0?
	if (hourGlass.spin < 0.0f)
	{
		hourGlass.spin += 360.0f;
	}

	// Increase The hourglass fy Variable
	hourGlass.fy += steps[adjust];

	// Is The hourglass fx Variable Equal To 0 And The fy Variable Greater Than 6000 Divided By The Current Level?
	if ((hourGlass.fx == 0) && (hourGlass.fy > 6000 / level))
	{
		// Play The Hourglass Appears Sound
		playSound("data/hourglass.wav", 0);

		// Give The Hourglass A Random X Value
		hourGlass.x = kiss.toInt() % 10 + 1;
		// Give The Hourglass A Random Y Value
		hourGlass.y = kiss.toInt() % 11;
		// Set hourglass fx Variable To One (Hourglass Stage)
		hourGlass.fx = 1;
		// Set hourglass fy Variable To Zero (Counter)
		hourGlass.fy = 0;
	}

	// Is The hourglass fx Variable Equal To 1 And The fy Variable Greater Than 6000 Divided By The Current Level?
	if ((hourGlass.fx == 1) && (hourGlass.fy > 6000 / level))
	{
		// Set fx To Zero (Hourglass Will Vanish)
		hourGlass.fx = 0;
		// Set fy to Zero (Counter Is Reset)
		hourGlass.fy = 0;
	}

	// Is The hourglass fx Variable Equal To 2 And The fy Variable Greater Than 500 Plus 500 Times The Current Level?
	if ((hourGlass.fx == 2) && (hourGlass.fy > 500 + (500 * level)))
	{
		// Kill The Freeze Sound
		playSound(null, 0);

		// Set hourglass fx Variable To Zero
		hourGlass.fx = 0;
		// Set hourglass fy Variable To Zero
		hourGlass.fy = 0;
	}
	delay++;
}

/**
 * Resets the player character and all the enemies.
 */
void resetObjects()
{
	player.x = 0; // Reset Player X Position To Far Left Of The Screen
	player.y = 0; // Reset Player Y Position To The Top Of The Screen
	player.fx = 0; // Set Fine X Position To Match
	player.fy = 0; // Set Fine Y Position To Match

	// Create the enemies for the next level/stage
	enemies = new GameObject[stage * level];
	// Loop Through All The Enemies
	foreach (inout enemy; enemies)
	{
		// A Random X Position
		enemy.x = 5 + kiss.toInt() % 6;
		// A Random Y Position
		enemy.y = kiss.toInt() % 11;
		// Set Fine X To Match
		enemy.fx = enemy.x * 60;
		// Set Fine Y To Match
		enemy.fy = enemy.y * 40;
	}
}

/**
 * Load the texture used in this tutorial.
 */
void loadGLTextures()
{
	SDL_Surface* imageTexture;
	SDL_Surface* fontTexture;

	// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
	if ((fontTexture = SDL_LoadBMP("data/Font.bmp")) !is null && (imageTexture = SDL_LoadBMP(
		"data/image.bmp")) !is null)
	{
		// Free the surface when exiting the scope
		scope(exit)
			SDL_FreeSurface(fontTexture);
		scope(exit)
			SDL_FreeSurface(imageTexture);

		// Create The Texture
		glGenTextures(2, &textures[0]);

		// Create Nearest Filtered Texture
		glBindTexture(GL_TEXTURE_2D, textures[0]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, fontTexture.w, fontTexture.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, fontTexture.pixels);

		// Create Nearest Filtered Texture
		glBindTexture(GL_TEXTURE_2D, textures[1]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexImage2D(GL_TEXTURE_2D, 0, 3, imageTexture.w, imageTexture.h, 0,
			GL_BGR, GL_UNSIGNED_BYTE, imageTexture.pixels);
	}
}

/**
 * Function to build our font list. 
 */
void buildFont()
{
	// Creating 256 Display List
	base = glGenLists(256);
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, textures[0]);

	// Loop Through All 256 Lists
	for (int i = 0; i < 256; i++)
	{
		/* NOTE:
		 *  BMPs are stored with the top-leftmost pixel being the
		 * last byte and the bottom-rightmost pixel being the first
		 * byte. So an image that is displayed as
		 *    1 0
		 *    0 0
		 * is represented data-wise like
		 *    0 0
		 *    0 1
		 * And because SDL_LoadBMP loads the raw data without
		 * translating to how it is thought of when viewed we need
		 * to start at the bottom-right corner of the data and work
		 * backwards to get everything properly. So the below code has
		 * been modified to reflect this. Examine how this is done and
		 * how the original tutorial is done to grasp the differences.
		 *
		 * As a side note BMPs are also stored as BGR instead of RGB
		 * and that is why we load the texture using GL_BGR. It's
		 * bass-ackwards I know but whattaya gonna do?
		 */

		// X Position Of Current Character
		float cx = 1 - cast(float) (i % 16) / 16.0f;
		// Y Position Of Current Character
		float cy = 1 - cast(float) (i / 16) / 16.0f;

		// Start Building A List
		glNewList(base + (255 - i), GL_COMPILE);
		{
			// Use A Quad For Each Character
			glBegin(GL_QUADS);
			{
				// Texture Coord (Bottom Left)
				glTexCoord2f(cx - 0.0625, cy);
				// Vertex Coord (Bottom Left)
				glVertex2i(0, 16);

				// Texture Coord (Bottom Right)
				glTexCoord2f(cx, cy);
				// Vertex Coord (Bottom Right)
				glVertex2i(16, 16);

				// Texture Coord (Top Right)
				glTexCoord2f(cx, cy - 0.0625f);
				// Vertex Coord (Top Right)
				glVertex2i(16, 0);

				// Texture Coord (Top Left)
				glTexCoord2f(cx - 0.0625f, cy - 0.0625f);
				// Vertex Coord (Top Left)
				glVertex2i(0, 0);
			}
			glEnd();

			// Move To The Left Of The Character
			glTranslated(15, 0, 0);
		}
		glEndList();
	}
}

/**
 * Resize and initialize the OpenGL window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);

	// Reset The Projection Matrix
	glLoadIdentity();

	// Set our ortho view
	glOrtho(0.0f, width, height, 0.0f, -1.0f, 1.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);

	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * Initialize OpenGL.
 */
void initGL()
{
	// Jump To Texture Loading Routine
	loadGLTextures();

	// Build The Font
	buildFont();

	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	// Enable Blending
	glEnable(GL_BLEND);
	// Type Of Blending To Use
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

/**
 * This Is Where The Printing Happens.
 * 
 * Params:
 *     x = the x coordinate of the text
 *     y = the y coordinate of the text
 *     set = font set
 *     string = the text to be printed
 */
void glPrint(GLint x, GLint y, int set, char[] string)
{
	if (set > 1)
	{
		set = 1;
	}
	// Enable textures
	glEnable(GL_TEXTURE_2D);
	// Select Our Font Texture
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	// Disables Depth Testing
	glDisable(GL_DEPTH_TEST);
	// Reset The Modelview Matrix
	glLoadIdentity();
	// Position The Text (0,0 - Bottom Left)
	glTranslated(x, y, 0);
	// Choose The Font Set (0 or 1)
	glListBase(base - 32 + (128 * set));
	// If Set 0 Is Being Used Enlarge Font
	if (set == 0)
	{
		// Enlarge Font Width And Height
		glScalef(1.5f, 2.0f, 1.0f);
	}
	// Write The Text To The Screen
	glCallLists(string.length, GL_UNSIGNED_BYTE, toStringz(string));
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// Disable textures
	glDisable(GL_TEXTURE_2D);
}

/**
 * Here goes our drawing code 
 */
void drawGLScene()
{
	// These are to calculate our fps
	static GLint to = 0;
	static GLint frames = 0;

	// Clear The Screen And The Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();

	// Set Color To Purple
	glColor3f(1.0f, 0.5f, 1.0f);
	// Write GRID CRAZY On The Screen
	glPrint(207, 24, 0, "GRID CRAZY");
	// Set Color To Yellow
	glColor3f(1.0f, 1.0f, 0.0f);
	// Write Actual Level Stats
	glPrint(20, 20, 1, "Level: " ~ Int.toString(displayedLevel));
	// Write Stage Stats
	glPrint(20, 40, 1, "Stage: " ~ Int.toString(stage));

	// Is The Game Over?
	if (gameover)
	{
		// Pick A Random Color
		glColor3ub(kiss.toInt() % 255, kiss.toInt() % 255, kiss.toInt() % 255);
		// Write GAME OVER To The Screen
		glPrint(472, 20, 1, "GAME OVER");
		// Write PRESS SPACE To The Screen
		glPrint(456, 40, 1, "PRESS SPACE");
	}

	// Loop Through Lives Minus Current Life
	for (int loop = 0; loop < lives - 1; loop++)
	{
		// Reset The View
		glLoadIdentity();
		// Move To The Right Of Our Title Text
		glTranslatef(490 + (loop * 40.0f), 40.0f, 0.0f);
		// Rotate Counter Clockwise
		glRotatef(-player.spin, 0.0f, 0.0f, 1.0f);
		// Set Player Color To Light Green
		glColor3f(0.0f, 1.0f, 0.0f);

		// Start Drawing Our Player Using Lines
		glBegin(GL_LINES);
		glVertex2d(-5, -5); // Top Left Of Player
		glVertex2d(5, 5); // Bottom Right Of Player
		glVertex2d(5, -5); // Top Right Of Player
		glVertex2d(-5, 5); // Bottom Left Of Player
		glEnd();

		// Rotate Counter Clockwise
		glRotatef(-player.spin * 0.5f, 0.0f, 0.0f, 1.0f);
		// Set Player Color To Dark Green
		glColor3f(0.0f, 0.75f, 0.0f);

		// Start Drawing Our Player Using Lines
		glBegin(GL_LINES);
		glVertex2d(-7, 0); // Left Center Of Player
		glVertex2d(7, 0); // Right Center Of Player
		glVertex2d(0, -7); // Top Center Of Player
		glVertex2d(0, 7); // Bottom Center Of Player
		glEnd();
	}

	// Set Filled To True Before Testing
	filled = true;
	// Set Line Width For Cells To 2.0f
	glLineWidth(2.0f);
	// Disable Antialiasing
	glDisable(GL_LINE_SMOOTH);
	// Reset The Current Modelview Matrix
	glLoadIdentity();

	// Loop From Left To Right
	for (int loop1 = 0; loop1 < 11; loop1++)
	{
		// Loop From Top To Bottom
		for (int loop2 = 0; loop2 < 11; loop2++)
		{
			// Set Line Color To Blue
			glColor3f(0.0f, 0.5f, 1.0f);

			// Has The Horizontal Line Been Traced
			if (hline[loop1][loop2])
			{
				glColor3f(1.0f, 1.0f, 1.0f);
			}

			// Dont Draw To Far Right
			if (loop1 < 10)
			{
				// If A Horizontal Line Isn't Filled
				if (!hline[loop1][loop2])
				{
					filled = false;
				}

				// Start Drawing Horizontal Cell Borders
				glBegin(GL_LINES);
				// Left Side Of Horizontal Line
				glVertex2d(20 + (loop1 * 60), 70 + (loop2 * 40));
				// Right Side Of Horizontal Line
				glVertex2d(80 + (loop1 * 60), 70 + (loop2 * 40));
				glEnd();
			}

			// Set Line Color To Blue
			glColor3f(0.0f, 0.5f, 1.0f);

			// Has The Horizontal Line Been Traced
			if (vline[loop1][loop2])
			{
				// If So, Set Line Color To White
				glColor3f(1.0f, 1.0f, 1.0f);
			}

			// Dont Draw To Far Down
			if (loop2 < 10)
			{
				// If A Verticle Line Isn't Filled
				if (!vline[loop1][loop2])
				{
					filled = false;
				}

				// Start Drawing Verticle Cell Borders
				glBegin(GL_LINES);
				// Left Side Of Horizontal Line
				glVertex2d(20 + (loop1 * 60), 70 + (loop2 * 40));
				// Right Side Of Horizontal Line
				glVertex2d(20 + (loop1 * 60), 110 + (loop2 * 40));
				glEnd();
			}

			// Enable Texture Mapping
			glEnable(GL_TEXTURE_2D);
			// Bright White Color
			glColor3f(1.0f, 1.0f, 1.0f);
			// Select The Tile Image
			glBindTexture(GL_TEXTURE_2D, textures[1]);

			// If In Bounds, Fill In Traced Boxes
			if ((loop1 < 10) && (loop2 < 10))
			{
				// Are All Sides Of The Box Traced?
				if (hline[loop1][loop2] && hline[loop1][loop2 + 1] && vline[loop1][loop2] && vline[loop1 + 1][loop2])
				{
					// Draw A Textured Quad
					glBegin(GL_QUADS);
					// Top Right
					glTexCoord2f(cast(float) (loop1 / 10.0f) + 0.1f,
						1.0f - (cast(float) (loop2 / 10.0f)));
					glVertex2d(20 + (loop1 * 60) + 59, 70 + loop2 * 40 + 1);
					// Top Left
					glTexCoord2f(cast(float) (loop1 / 10.0f),
						1.0f - (cast(float) (loop2 / 10.0f)));
					glVertex2d(20 + (loop1 * 60) + 1, 70 + loop2 * 40 + 1);
					// Bottom Left
					glTexCoord2f(cast(float) (loop1 / 10.0f),
						1.0f - (cast(float) (loop2 / 10.0f) + 0.1f));
					glVertex2d(20 + (loop1 * 60) + 1, (70 + loop2 * 40) + 39);
					// Bottom Right
					glTexCoord2f(cast(float) (loop1 / 10.0f) + 0.1f,
						1.0f - (cast(float) (loop2 / 10.0f) + 0.1f));
					glVertex2d(20 + (loop1 * 60) + 59, (70 + loop2 * 40) + 39);
					glEnd();
				}
			}

			// Disable Texture Mapping
			glDisable(GL_TEXTURE_2D);
		}
	}

	// Set The Line Width To 1.0f
	glLineWidth(1.0f);

	// Is Anti true?
	if (anti)
	{
		glEnable(GL_LINE_SMOOTH);
	}

	// If fx = 1 Draw The Hourglass
	if (hourGlass.fx == 1)
	{
		// Reset The Modelview Matrix
		glLoadIdentity();
		// Move To The Fine Hourglass Position
		glTranslatef(20.0f + (hourGlass.x * 60), 70.0f + (hourGlass.y * 40),
			0.0f);
		// Rotate Clockwise
		glRotatef(hourGlass.spin, 0.0f, 0.0f, 1.0f);
		// Set Hourglass Color To Random Color
		glColor3ub(kiss.toInt() % 255, kiss.toInt() % 255, kiss.toInt() % 255);

		// Start Drawing Our Hourglass Using Lines
		glBegin(GL_LINES);
		// Top Left Of Hourglass
		glVertex2d(-5, -5);
		// Bottom Right Of Hourglass
		glVertex2d(5, 5);
		// Top Right Of Hourglass
		glVertex2d(5, -5);
		// Bottom Left Of Hourglass
		glVertex2d(-5, 5);
		// Bottom Left Of Hourglass
		glVertex2d(-5, 5);
		// Bottom Right Of Hourglass
		glVertex2d(5, 5);
		// Top Left Of Hourglass
		glVertex2d(-5, -5);
		// Top Right Of Hourglass
		glVertex2d(5, -5);
		glEnd();
	}

	// Reset The Modelview Matrix
	glLoadIdentity();
	// Move To The Find Player Position
	glTranslatef(player.fx + 20.0f, player.fy + 70.0f, 0.0f);
	// Rotate Clockwise
	glRotatef(player.spin, 0.0f, 0.0f, 1.0f);
	// Set Player Color To Light Green
	glColor3f(0.0f, 1.0f, 0.0f);

	// Start Drawing Our Player Using Lines
	glBegin(GL_LINES);
	// Top Left Of Player
	glVertex2d(-5, -5);
	// Bottom Right Of Player
	glVertex2d(5, 5);
	// Top Right Of Player
	glVertex2d(5, -5);
	// Bottom Left Of Player
	glVertex2d(-5, 5);
	glEnd();

	// Rotate Clockwise
	glRotatef(player.spin * 0.5f, 0.0f, 0.0f, 1.0f);
	// Set Player Color To Dark Green
	glColor3f(0.0f, 0.75f, 0.0f);
	// Start Drawing Our Player Using Lines
	glBegin(GL_LINES);
	// Left Center Of Player
	glVertex2d(-7, 0);
	// Right Center Of Player
	glVertex2d(7, 0);
	// Top Center Of Player
	glVertex2d(0, -7);
	// Bottom Center Of Player
	glVertex2d(0, 7);
	glEnd();

	// Loop To Draw Enemies
	foreach (enemy; enemies)
	{
		// Reset The Modelview Matrix
		glLoadIdentity();
		glTranslatef(enemy.fx + 20.0f, enemy.fy + 70.0f, 0.0f);
		// Make Enemy Body Pink
		glColor3f(1.0f, 0.5f, 0.5f);

		// Start Drawing Enemy
		glBegin(GL_LINES);
		// Top Point Of Body
		glVertex2d(0, -7);
		// Left Point Of Body
		glVertex2d(-7, 0);
		// Left Point Of Body
		glVertex2d(-7, 0);
		// Bottom Point Of Body
		glVertex2d(0, 7);
		// Bottom Point Of Body
		glVertex2d(0, 7);
		// Right Point Of Body
		glVertex2d(7, 0);
		// Right Point Of Body
		glVertex2d(7, 0);
		// Top Point Of Body
		glVertex2d(0, -7);
		glEnd();

		// Rotate The Enemy Blade */
		glRotatef(enemy.spin, 0.0f, 0.0f, 1.0f);
		// Make Enemy Blade Red */
		glColor3f(1.0f, 0.0f, 0.0f);

		// Start Drawing Enemy Blade
		glBegin(GL_LINES);
		// Top Left Of Enemy
		glVertex2d(-7, -7);
		// Bottom Right Of Enemy
		glVertex2d(7, 7);
		// Bottom Left Of Enemy
		glVertex2d(-7, 7);
		// Top Right Of Enemy
		glVertex2d(7, -7);
		glEnd();
	}

	// Gather our frames per second
	frames++;
	{
		GLint time = SDL_GetTicks();
		if (time - to >= 5000)
		{
			GLfloat seconds = (time - to) / 1000.0;
			GLfloat fps = frames / seconds;
			Stdout(frames)(" frames in ")(seconds)(" seconds = ")(fps)(" FPS").newline;
			to = time;
			frames = 0;
		}
	}
}

/**
 * Play the given sound.
 * 
 * Params:
 *     sound = the sound to be played
 *     repeat = the repeat count, -1 means infinite
 */
void playSound(char[] sound, int repeat)
{
	if (sound is null)
	{
		Mix_HaltChannel(1);
		Mix_FreeChunk(chunk);
		chunk = null;
		return;
	}

	if (chunk !is null)
	{
		Mix_HaltChannel(1);
		Mix_FreeChunk(chunk);
		chunk = null;
	}

	chunk = Mix_LoadWAV(sound);
	if (chunk is null)
	{
		throw new Exception(
			"Failed to load sound: '" ~ sound ~ "': " ~ getMixError());
	}
	Mix_PlayChannel(-1, chunk, repeat);
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits,
	bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	// Note the SDL_DOUBLEBUF flag is not required to enable double
	// buffering when setting an OpenGL video mode.
	// Double buffering is enabled or disabled using the
	// SDL_GL_DOUBLEBUFFER attribute. (See above.)
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}

/**
 * Get the SDL Mixer error as a D string.
 * 
 * Returns: A D string containing the current SDL Mixer error.
 */
char[] getMixError()
{
	return fromStringz(Mix_GetError());
}
